/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package dsproject;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;

/**
 *
 * @author Honoré
 */
public class Simulator {

    final int idOfReportingRequest = -1;
    int seqRqId = 0;
    int totalBandwidth = 0;
    int nbMessage = 0;
    boolean newTT;
    final int nLoop;
    final int nRequest;
    Network net;
    String filename;
    Simulator.TYPE type;
    ArrayList<Request> requestInProcess;
    HashMap<Integer, ArrayList<Request>> timeTable;
    public final String DEFAULT_FILE_NAME = "test";

    public Simulator(int nLoop, int nRequest, Network net, String filename, Boolean newTT) {
        this.nLoop = nLoop;
        this.net = net;
        this.nRequest = nRequest;
        this.filename = filename;
        this.newTT = newTT;
    }

    public enum TYPE {

        CLASSIC, // The classic/previous scheme
        CACHE, // The Fully-Cached Scheme
        CACHEv2, // The enhanced cached scheme
        CACHEvH // The hierarchy based cache  system
    }

    /**
     * This class allow to launch a simulation, using the timeTable of Request
     * @author Honoré
     * @return the amount of bandwidth used
     * @see    Request
     */
    public Report process(Simulator.TYPE type) throws FileNotFoundException, ClassNotFoundException, IOException {

        int i;
        Request rq;
        //Initialize the values between each process
        initialize(type);
        i = Collections.min(timeTable.keySet());
        //Main simulation loop
        while (i < nLoop || requestInProcess.size() > 0) {

            //Get the new resources to launch
            if (i < nLoop && timeTable.get(i) != null) {
                for (Request req : timeTable.get(i)) {
                    requestInProcess.add(req);
                }
            }
            //System.out.println("loop "+ i + ":" + requestInProcess);
            //process all the ongoing request
            for (Iterator<Request> iter = requestInProcess.iterator(); iter.hasNext();) {

                rq = (Request) iter.next();
                totalBandwidth += rq.operate(i, type);
                if (rq.isCompleted()) {
                    iter.remove();
                } else {
                    nbMessage++;
                }
            }
            i++;
        }
        return new Report(type, totalBandwidth, nbMessage, nLoop, i, nRequest);
    }

    private Request generateRequest() {
        Random rdGene = new Random();
        Node source, destination;
        String selec;
        HashSet<ResourceState.TYPE> rsNeed = new HashSet<ResourceState.TYPE>();
        int rd1, rd2;
        int i = 0;

        //Select a random source Node & destination Node
        rd1 = rdGene.nextInt(net.nodes.length);
        source = net.nodes[rd1];
        rd2 = rdGene.nextInt(net.nodes.length - 1);
        destination = net.nodes[rd2 >= rd1 ? rd2 + 1 : rd2];
        //Select randomly the type and number of ressource to request
        selec = Integer.toBinaryString((int) rdGene.nextInt((int) Math.pow(2, ResourceState.TYPE.values().length) - 1) + 1);
        for (ResourceState.TYPE rs : ResourceState.TYPE.values()) {
            if (i < selec.length() && selec.charAt(i) == '1') {
                rsNeed.add(rs);
            }
            i++;
        }
        return new Request(net.shortestPath(source, destination), rsNeed, seqRqId++, Message.TYPE.NORMAL);
    }

    /**
     * Create a new Time Table, according to
     * @param nRequest
     * @return
     */
    private HashMap<Integer, ArrayList<Request>> generateTimeTable(int nRequest) {

        Integer startTime;
        int time;
        Request rq;
        ArrayList<Node> arr;
        Random rdGene = new Random();
        HashSet<ResourceState.TYPE> rsNeed;
        HashMap<Integer, ArrayList<Request>> rqtt = new HashMap<Integer, ArrayList<Request>>(nLoop);

        //Each node reports its resource information periodically to its corresponding cluster head
        for (ResourceState.TYPE rs : ResourceState.TYPE.values()) {
            for (Node nd : net.nodes) {
                if (nd.getClStatus() == Node.STATUS.CLAIMED) {
                    time = -2;
                    //Build the path's node list on the reverse side, because its a one-way request.
                    arr = new ArrayList<Node>();
                    arr.add(nd.getBelong());
                    arr.add(nd);
                    //Build the Request Body
                    rsNeed = new HashSet<ResourceState.TYPE>();
                    rsNeed.add(rs);
                    while (time < nLoop) {
                        if (rqtt.get(time) == null) {
                            rqtt.put(time, new ArrayList<Request>());
                        }
                        rqtt.get(time).add(new Request(new Path(arr, false), rsNeed, idOfReportingRequest, Message.TYPE.PERIODIC));
                        time += rs.getValidityTime();
                    }
                }
            }
        }

        //When resource information from a node outside a cluster is required, a reactive query is propagated through the cluster heads
        for (int i = 0; i < nRequest; i++) {
            //Create a request and assign a random start time
            startTime = rdGene.nextInt(this.nLoop);
            if (rqtt.get(startTime) == null) {
                rqtt.put(startTime, new ArrayList<Request>());
            }
            rqtt.get(startTime).add(this.generateRequest());
        }
        //
        return rqtt;
    }

    private void saveTimeTable() {

        FileOutputStream fos = null;
        ObjectOutputStream out = null;
        try {
            fos = new FileOutputStream(filename);
            out = new ObjectOutputStream(fos);
            out.writeObject(this.timeTable);
            out.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void initialize(Simulator.TYPE type) throws FileNotFoundException, IOException, ClassNotFoundException {
        this.seqRqId = 0;
        this.totalBandwidth = 0;
        this.nbMessage = 0;
        this.type = type;
        this.requestInProcess = new ArrayList<Request>();
        System.out.println("************************* PROCESSING " + this.type.toString() + " ****************************************************");

        if (this.newTT) {
            this.timeTable = generateTimeTable(nRequest);
            this.saveTimeTable();
            this.newTT = false;
        } else {
            FileInputStream f_in = new FileInputStream(filename != null ? filename : DEFAULT_FILE_NAME);
            // Read object using ObjectInputStream
            ObjectInputStream obj_in = new ObjectInputStream(f_in);
            // Read an object
            timeTable = (HashMap<Integer, ArrayList<Request>>) obj_in.readObject();
        }
    }
}
